﻿/**
 * @copyright Copyright 2009 Structured Solutions
 * @license http://www.codeplex.com/ShippingAgent/license Microsoft Public License (Ms-PL)
 * @version 3.2
 */

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Security.Cryptography;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using BVSoftware.Bvc5.Core;
using BVSoftware.Bvc5.Core.BusinessRules;
using BVSoftware.Bvc5.Core.Catalog;
using BVSoftware.Bvc5.Core.Contacts;
using BVSoftware.Bvc5.Core.Content;
using BVSoftware.Bvc5.Core.Membership;
using BVSoftware.Bvc5.Core.Orders;
using BVSoftware.Bvc5.Core.Shipping;

namespace StructuredSolutions.ShippingAgent
{
    [WebService(Namespace = "http://www.codeplex.com/ShippingAgent/2009-01-01",
        Description = "Shipping Agent for BVC5 SP3")]
    public class ShippingAgentImpl : ShippingAgentSoap
    {
        #region WebMethod Implementations

        /// <summary>
        /// Return orders placed since lastOrderNumber. If lastOrderNumber is null or an empty
        /// string, then return all orders from today.
        /// </summary>
        /// <param name="pageKey">The last order number in the previous results. May be
        /// null or an empty string if this is the first time this method is called.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount OrderResults, sorted by order number; or an empty list or null 
        /// if there are no new orders.</returns>
        /// <remarks>
        /// <para>If maxCount orders are returned, the Shipper client will call this method again with the 
        /// same ranges and with lastOrderNumber set to the last order number in the previous result. In this
        /// case, use lastOrderNumber to calculate an appropriate new startOrderNumber (for example,
        /// startOrderNumber = lastOrderNumber + 1).</para>
        /// <para>Return a <see cref="SoapException"/> if the parameters are invalid.</para>
        /// </remarks>
        [WebMethod(Description = "Retrieve orders placed since lastOrderNumber.")]
        public override OrderResultsPage GetNewOrders(string pageKey, int maxCount)
        {
            try
            {
                return GetNewOrdersFromStore(pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve new orders.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return one order.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <returns>The OrderResult.</returns>
        /// <exception cref="SoapException">If the orderID is not valid (such as blank or null).</exception>
        [WebMethod(Description = "Retrieve one order.")]
        public override OrderResult GetOrder(string orderId)
        {
            try
            {
                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                return GetOrderFromStore(orderId);
            }
            catch (ApplicationException ex)
            {
                var result = new OrderResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new OrderResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        /// <summary>
        /// Return the current inventory for all items in one order.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <returns>A list of item Inventory objects or null if inventory is not tracked.</returns>
        /// <exception cref="SoapException">If the orderID is not valid (such as blank or null).</exception>
        [WebMethod(Description = "Retrieve the current inventory for all items in one order.")]
        public override InventoryResult GetOrderInventory(string orderId)
        {
            try
            {
                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                return GetOrderInventoryFromStore(orderId);
            }
            catch (ApplicationException ex)
            {
                var result = new InventoryResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new InventoryResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        /// <summary>
        /// Return all the orders that fall on and within the specified ranges.
        /// </summary>
        /// <param name="startOrderNumber">The starting order number; or null or an empty string to start at the first order number in the store.</param>
        /// <param name="endOrderNumber">The ending order number; or null or an empty string to end with the last order number in the store.</param>
        /// <param name="startOrderDate">The starting order date. If no start date is specified, start from the earliest order.</param>
        /// <param name="startOrderDateSpecified">True if startOrderDate is specified, false otherwise.</param>
        /// <param name="endOrderDate">The ending order date. If no end date is specified, end with the most recent order.</param>
        /// <param name="endOrderDateSpecified">True if endOrderDate is specified, false otherwise.</param>
        /// <param name="pageKey">The last order number from the previous result; or null or an empty string to ignore this parameter.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount Order's, sorted by order number; or an empty list or null 
        /// if there are no orders that fall on and within the specified ranges.</returns>
        /// <remarks>If maxCount orders are returned, the Shipper client will call this method again with the 
        /// same ranges and with lastOrderNumber set to the last order number in the previous result. In this
        /// case, use lastOrderNumber to calculate an appropriate new startOrderNumber (for example,
        /// startOrderNumber = lastOrderNumber + 1).
        /// </remarks>
        [WebMethod(Description = "Return the list of orders that fall on and within the specified ranges.")]
        public override OrderResultsPage GetOrdersByRange(string startOrderNumber, string endOrderNumber, DateTime startOrderDate, bool startOrderDateSpecified, DateTime endOrderDate, bool endOrderDateSpecified, string pageKey, int maxCount)
        {
            try
            {
                var nullableStartDate = ShippingAgentHelper.ConvertWebServiceNullable(startOrderDate, startOrderDateSpecified);
                var nullableEndDate = ShippingAgentHelper.ConvertWebServiceNullable(endOrderDate, endOrderDateSpecified);
                return GetOrdersByRangeFromStore(startOrderNumber, endOrderNumber, nullableStartDate, nullableEndDate, pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve orders by range.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return one specific package.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <param name="packageId">The ID of the package.</param>
        /// <returns>A PackageResult.</returns>
        /// <exception cref="SoapException">If the orderId or packageId is not valid (such as blank or null).</exception>
        [WebMethod(Description = "Retrieve one package.")]
        public override PackageResult GetPackage(string orderId, string packageId)
        {
            try
            {
                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(packageId))
                    throw new ApplicationException("Invalid package ID.");
                return GetPackageFromStore(orderId, packageId);
            }
            catch (ApplicationException ex)
            {
                var result = new PackageResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new PackageResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        /// <summary>
        /// Returns the complete list of tracking providers supported by the store. The
        /// Shipper client may assign any tracking provider to a package, regardless of
        /// how the shipping cost was initially calculated.
        /// </summary>
        /// <returns>The list of TrackingProvider's.</returns>
        [WebMethod(Description = "Retrieve the list of package tracking providers.")]
        public override TrackingProvider[] GetTrackingProviders()
        {
            try
            {
                return GetTrackingProvidersFromStore().ToArray();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve tracking providers.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Record package information.
        /// </summary>
        /// <param name="update">The package information to record.</param>
        /// <returns>If the update is successful, the UpdateResult contains a fresh
        /// copy of the Order that contains the package. If the update is not
        /// successful, the UpdateResult contains the reason why the update failed.</returns>
        /// <remarks>The Shipper client may not include all Update elements. null
        /// elements should be ignored. If the ShipDate is included, then the package
        /// should be marked shipped.</remarks>
        [WebMethod(Description = "Record package information.")]
        public override UpdateResult UpdatePackage(Update update)
        {
            try
            {
                if (update == null)
                    return null;
                if (string.IsNullOrEmpty(update.OrderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(update.PackageId))
                    throw new ApplicationException("Invalid package ID.");
                return UpdatePackageInStore(update);
            }
            catch (ApplicationException ex)
            {
                var result = new UpdateResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                var result = new UpdateResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        #endregion

        #region Platform Specific Methods

        private const string NullProviderId = "0567206B-9AFA-4063-BF6F-B8C096706E92";

        protected static OrderResultsPage GetNewOrdersFromStore(string pageKey, int maxCount)
        {
            var orders = new List<OrderResult>();

            var criteria = new OrderSearchCriteria();
            criteria.IsPlaced = true;

            // If lastOrderNumber is missing, then return orders from today.
            if (string.IsNullOrEmpty(pageKey))
                criteria.StartDate = DateTime.Today;
            else
            {
                var tempCriteria = new OrderSearchCriteria();
                tempCriteria.IsPlaced = true;
                tempCriteria.OrderNumber = pageKey;
                foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(tempCriteria))
                {
                    criteria.StartDate = bvOrderLite.TimeOfOrder;
                    break;
                }
            }

            foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(criteria))
            {
                if (!string.IsNullOrEmpty(pageKey) && bvOrderLite.OrderNumber.CompareTo(pageKey) <= 0)
                    continue;

                // Skip orders that are on hold
                if (bvOrderLite.StatusCode.Equals("88B5B4BE-CA7B-41a9-9242-D96ED3CA3135", StringComparison.InvariantCultureIgnoreCase))
                    continue;

                var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvOrderLite.Bvin);
                orders.Add(BuildOrderResult(bvOrder));
                if (orders.Count == maxCount) break;
            }

            if (orders.Count == 0)
                return null;

            // Sort the shipments by OrderNumber so that the most recent OrderNumber is last
            orders.Sort((x, y) => x.Order.OrderNumber.CompareTo(y.Order.OrderNumber));

            var result = new OrderResultsPage();
            result.Key = orders[orders.Count - 1].Order.OrderNumber;
            result.OrderResults = orders.ToArray();
            return result;
        }

        protected static OrderResult GetOrderFromStore(string orderId)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(orderId);
            if (bvOrder == null || !bvOrder.Bvin.Equals(orderId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid order ID.");

            return BuildOrderResult(bvOrder);
        }

        protected static InventoryResult GetOrderInventoryFromStore(string orderId)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(orderId);
            if (bvOrder == null || !bvOrder.Bvin.Equals(orderId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid order ID.");

            return BuildInventoryResult(bvOrder);
        }

        protected static OrderResultsPage GetOrdersByRangeFromStore(string startOrderNumber, string endOrderNumber, DateTime? startOrderDate,
                                                                 DateTime? endOrderDate, string pageKey, int maxCount)
        {
            var orders = new List<OrderResult>();

            var criteria = new OrderSearchCriteria();
            criteria.IsPlaced = true;
            if (startOrderDate.HasValue) criteria.StartDate = startOrderDate.Value;
            if (endOrderDate.HasValue) criteria.EndDate = endOrderDate.Value;

            // BVC5 only supports order date ranges, so convert order number range to dates to 
            // reduce the number of orders in the result set
            if (!string.IsNullOrEmpty(startOrderNumber))
            {
                var tempCriteria = new OrderSearchCriteria();
                tempCriteria.IsPlaced = true;
                tempCriteria.OrderNumber = startOrderNumber;
                foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(tempCriteria))
                {
                    if (bvOrderLite.TimeOfOrder > criteria.StartDate)
                        criteria.StartDate = bvOrderLite.TimeOfOrder;
                    break;
                }
            }
            if (!string.IsNullOrEmpty(endOrderNumber))
            {
                var tempCriteria = new OrderSearchCriteria();
                tempCriteria.IsPlaced = true;
                tempCriteria.OrderNumber = endOrderNumber;
                foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(tempCriteria))
                {
                    if (bvOrderLite.TimeOfOrder < criteria.EndDate)
                        criteria.EndDate = bvOrderLite.TimeOfOrder;
                    break;
                }
            }
            if (!string.IsNullOrEmpty(pageKey))
            {
                var tempCriteria = new OrderSearchCriteria();
                tempCriteria.IsPlaced = true;
                tempCriteria.OrderNumber = pageKey;
                foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(tempCriteria))
                {
                    if (bvOrderLite.TimeOfOrder > criteria.StartDate)
                        criteria.StartDate = bvOrderLite.TimeOfOrder;
                    break;
                }
            }

            foreach (var bvOrderLite in BVSoftware.Bvc5.Core.Orders.Order.FindByCriteria(criteria))
            {
                if (!string.IsNullOrEmpty(startOrderNumber) && bvOrderLite.OrderNumber.CompareTo(startOrderNumber) < 0)
                    continue;
                if (!string.IsNullOrEmpty(pageKey) && bvOrderLite.OrderNumber.CompareTo(pageKey) == 0)
                    continue;
                if (!string.IsNullOrEmpty(endOrderNumber) && bvOrderLite.OrderNumber.CompareTo(endOrderNumber) > 0)
                    break;

                var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvOrderLite.Bvin);
                orders.Add(BuildOrderResult(bvOrder));
                if (orders.Count == maxCount) break;
            }

            if (orders.Count == 0)
                return null;

            // Sort the shipments by OrderNumber so that the highest OrderNumber is last
            orders.Sort((x, y) => x.Order.OrderNumber.CompareTo(y.Order.OrderNumber));

            var result = new OrderResultsPage();
            result.Key = orders[orders.Count - 1].Order.OrderNumber;
            result.OrderResults = orders.ToArray();
            return result;
        }

        /// <summary>
        /// Return one shipment from the store.
        /// </summary>
        /// <param name="orderId">Ignored by this implementation.</param>
        /// <param name="packageId">The BVC5 Package bvin.</param>
        /// <returns>The Shipment that corresponds to the BVC5 Package.</returns>
        protected static PackageResult GetPackageFromStore(string orderId, string packageId)
        {
            var bvPackage = BVSoftware.Bvc5.Core.Shipping.Package.FindByBvin(packageId);
            if (bvPackage == null || !bvPackage.Bvin.Equals(packageId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid package ID.");

            return BuildPackageResult(bvPackage);
        }

        protected static List<TrackingProvider> GetTrackingProvidersFromStore()
        {
            var trackingProviders = new List<TrackingProvider>();
            foreach (var bvProvider in AvailableProviders.Providers)
            {
                if (!bvProvider.SupportsTracking) continue;

                TrackingProvider provider = new TrackingProvider();
                provider.Name = bvProvider.Name;
                provider.TrackingProviderId = bvProvider.ProviderId;
                trackingProviders.Add(provider);
            }
            return trackingProviders;
        }

        protected UpdateResult UpdatePackageInStore(Update update)
        {
            var bvPackage = BVSoftware.Bvc5.Core.Shipping.Package.FindByBvin(update.PackageId);
            if (bvPackage == null || !bvPackage.Bvin.Equals(update.PackageId, StringComparison.InvariantCultureIgnoreCase))
                throw new ApplicationException("Invalid package ID.");

            return BuildUpdateResult(update, bvPackage);
        }

        #endregion

        private static bool AddressesAreEqual(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, ShippingAddress shippingAddress, string shippingCountryId,
                                              string shippingRegionId)
        {
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.City, shippingAddress.City, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Company, shippingAddress.Company, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.CountryBvin, shippingCountryId, StringComparison.InvariantCultureIgnoreCase)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Fax, shippingAddress.Fax, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.FirstName, shippingAddress.FirstName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.LastName, shippingAddress.LastName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Line1, shippingAddress.Line1, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Line2, shippingAddress.Line2, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.Phone, shippingAddress.Phone, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.PostalCode, shippingAddress.PostalCode, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(bvAddress.RegionBvin, shippingRegionId, StringComparison.InvariantCultureIgnoreCase)) return false;
            if (bvAddress.Residential != shippingAddress.Residential) return false;
            return true;
        }

        private static Address BuildAddress(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, string email)
        {
            if (bvAddress == null) return null;

            Address address = new Address();
            address.AddressId = string.IsNullOrEmpty(bvAddress.Bvin) ? GetUniqueAddressId(bvAddress) : bvAddress.Bvin;
            address.City = ShippingAgentHelper.StringOrNull(bvAddress.City);
            address.Company = ShippingAgentHelper.StringOrNull(bvAddress.Company);
            address.Country = ShippingAgentHelper.StringOrNull(bvAddress.CountryName);
            if (!string.IsNullOrEmpty(bvAddress.CountryBvin))
            {
                var country = Country.FindByBvin(bvAddress.CountryBvin);
                if (country != null && country.Bvin.Equals(bvAddress.CountryBvin, StringComparison.InvariantCultureIgnoreCase))
                    address.Country = country.IsoCode;
            }
            address.Email = ShippingAgentHelper.StringOrNull(email);
            address.Fax = ShippingAgentHelper.StringOrNull(bvAddress.Fax);
            address.FirstName = ShippingAgentHelper.StringOrNull(bvAddress.FirstName);
            address.LastName = ShippingAgentHelper.StringOrNull(bvAddress.LastName);
            address.Line1 = ShippingAgentHelper.StringOrNull(bvAddress.Line1);
            address.Line2 = ShippingAgentHelper.StringOrNull(bvAddress.Line2);
            address.Phone = ShippingAgentHelper.StringOrNull(bvAddress.Phone);
            address.PostalCode = ShippingAgentHelper.StringOrNull(bvAddress.PostalCode);
            address.Region = ShippingAgentHelper.StringOrNull(bvAddress.RegionName);

            return address;
        }

        private static NamedAmount[] BuildCharges(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            const string Handling = "Handling";
            const string Shipping = "Shipping";
            const string Taxes = "Taxes";

            var charges = new NamedAmountList();
            if (bvOrder.HandlingTotal != 0) charges[Handling].Amount += bvOrder.HandlingTotal;
            if (bvOrder.ShippingTotal != 0) charges[Shipping].Amount += bvOrder.ShippingTotal;
            if (bvOrder.TaxTotal != 0) charges[Taxes].Amount += bvOrder.TaxTotal;
            if (bvOrder.TaxTotal2 != 0) charges[Taxes].Amount += bvOrder.TaxTotal2;
            return charges.Count > 0 ? charges.ToArray() : null;
        }

        private static Customer BuildCustomer(UserAccount user)
        {
            if (user == null) return null;

            Customer customer = new Customer();
            customer.CustomerId = user.Bvin;
            customer.CustomerUrl = BuildCustomerUrl("MyAccount_Orders.aspx", "");
            customer.Email = ShippingAgentHelper.StringOrNull(user.Email);
            customer.FirstName = ShippingAgentHelper.StringOrNull(user.FirstName);
            customer.LastName = ShippingAgentHelper.StringOrNull(user.LastName);
            customer.MerchantUrl = BuildMerchantUrl("BVAdmin/People/users_edit.aspx", "id=" + user.Bvin);
            customer.UserName = user.UserName;
            return customer;
        }

        private static string BuildCustomerUrl(string path, string query)
        {
            try
            {
                UriBuilder uri = new UriBuilder(WebAppSettings.SiteStandardRoot);
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static InventoryResult BuildInventoryResult(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var itemCounts = new List<ItemCount>();
            if (bvOrder.Items != null)
            {
                foreach (var bvItem in bvOrder.Items)
                {
                    if (bvItem.AssociatedProduct == null || !bvItem.AssociatedProduct.TrackInventory)
                        continue;

                    var bvInventory = ProductInventory.FindByBvin(bvItem.ProductId);
                    var itemCount = new ItemCount();
                    itemCount.ItemId = bvItem.Bvin;
                    itemCount.Count = Convert.ToDouble(bvInventory.QuantityAvailable);
                    itemCounts.Add(itemCount);
                }
                if (itemCounts.Count > 0)
                {
                    var text = string.Format(ShippingAgentHelper.OrderInventoryExportedText, HttpContext.Current == null ? "(no IP in request)" : HttpContext.Current.Request.UserHostAddress);
                    bvOrder.AddNote(new OrderNote { Note = text, NoteType = OrderNoteType.System });
                }
            }
            return new InventoryResult { Success = true, ItemCounts = itemCounts.Count > 0 ? itemCounts.ToArray() : null };
        }

        private static Item BuildItem(LineItem bvItem)
        {
            Item item = new Item();
            item.Code = bvItem.ProductSku;
            item.ItemId = bvItem.Bvin;
            item.LineTotal = bvItem.LineTotal;
            item.Name = bvItem.ProductName;
            item.Price = bvItem.AdjustedPrice;
            item.Quantity = Convert.ToDouble(bvItem.Quantity);
            item.Weight = Convert.ToDouble(bvItem.Quantity * bvItem.GetSingleItemWeight());

            var description = new List<string>();
            foreach (var bvInput in bvItem.Inputs)
            {
                if (bvInput.DisplayToCustomer)
                    description.Add(string.Format("{0}: {1}", bvInput.InputName, bvInput.InputDisplayValue));
            }
            foreach (var bvLineItemModifier in bvItem.Modifiers)
            {
                if (!bvLineItemModifier.DisplayToCustomer)
                    continue;

                var bvModifier = ProductModifier.FindByBvin(bvLineItemModifier.ModifierBvin);
                if (bvModifier != null && bvModifier.Bvin.Equals(bvLineItemModifier.ModifierBvin, StringComparison.InvariantCultureIgnoreCase))
                {
                    foreach (var bvModifierOption in bvModifier.ModifierOptions)
                    {
                        if (!bvModifierOption.Bvin.Equals(bvLineItemModifier.ModifierValue, StringComparison.InvariantCultureIgnoreCase))
                            continue;

                        description.Add(string.Format("{0}: {1}", bvLineItemModifier.ModifierName, bvModifierOption.DisplayText));
                        break;
                    }
                }
            }
            if (description.Count > 0) item.Description = string.Join("; ", description.ToArray());

            if (bvItem.AssociatedProduct != null)
            {
                item.Shippable = bvItem.AssociatedProduct.ShippingMode != ShippingMode.None;
                if (bvItem.AssociatedProduct.TrackInventory)
                {
                    var bvInventory = ProductInventory.FindByBvin(bvItem.ProductId);
                    item.Inventory = Convert.ToDouble(bvInventory.QuantityAvailable);
                }
                if (!string.IsNullOrEmpty(bvItem.AssociatedProduct.ManufacturerId))
                {
                    var bvManufacturer = Manufacturer.FindByBvin(bvItem.AssociatedProduct.ManufacturerId);
                    if (bvManufacturer != null &&
                        bvManufacturer.Bvin.Equals(bvItem.AssociatedProduct.ManufacturerId, StringComparison.InvariantCultureIgnoreCase))
                    {
                        item.Manufacturer = new Supplier();
                        item.Manufacturer.Email = ShippingAgentHelper.StringOrNull(bvManufacturer.EmailAddress);
                        item.Manufacturer.Name = ShippingAgentHelper.StringOrNull(bvManufacturer.DisplayName);
                        item.Manufacturer.SupplierId = bvManufacturer.Bvin;
                    }
                }
                if (!string.IsNullOrEmpty(bvItem.AssociatedProduct.VendorId))
                {
                    var bvVendor = Vendor.FindByBvin(bvItem.AssociatedProduct.VendorId);
                    if (bvVendor != null && bvVendor.Bvin.Equals(bvItem.AssociatedProduct.VendorId, StringComparison.InvariantCultureIgnoreCase))
                    {
                        item.Vendor = new Supplier();
                        item.Vendor.Email = ShippingAgentHelper.StringOrNull(bvVendor.EmailAddress);
                        item.Vendor.Name = ShippingAgentHelper.StringOrNull(bvVendor.DisplayName);
                        item.Vendor.SupplierId = bvVendor.Bvin;
                    }
                }
            }
            return item;
        }

        private static Item BuildItem(LineItem bvItem, decimal quantity)
        {
            var item = BuildItem(bvItem);
            item.Quantity = Convert.ToDouble(quantity);
            item.Weight = Convert.ToDouble(quantity * bvItem.GetSingleItemWeight());
            return item;
        }

        private static Item[] BuildItems(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var items = new List<Item>();
            var index = 0;
            foreach (var bvItem in bvOrder.Items)
            {
                var item = BuildItem(bvItem);
                item.OrderBy = index++;
                items.Add(item);
            }

            // This is normally wasted code. BVC5 only tracks discounts until the
            // order is placed. So this order will always have OrderDiscounts = 0,
            // and ShippingDiscounts = 0.
            if (bvOrder.OrderDiscounts != 0)
            {
                items.Add(new Item
                {
                    Code = "Discount",
                    ItemId = bvOrder.Bvin + "OrderDiscount",
                    LineTotal = bvOrder.OrderDiscounts,
                    Name = "Order Discount",
                    OrderBy = index++,
                    Price = bvOrder.OrderDiscounts,
                    Quantity = 1,
                    Shippable = false
                });
            }

            if (bvOrder.ShippingDiscounts != 0)
            {
                items.Add(new Item
                {
                    Code = "Discount",
                    ItemId = bvOrder.Bvin + "ShippingDiscount",
                    LineTotal = bvOrder.ShippingDiscounts,
                    Name = "Shipping Discount",
                    OrderBy = index,
                    Price = bvOrder.ShippingDiscounts,
                    Quantity = 1,
                    Shippable = false
                });
            }

            return items.Count > 0 ? items.ToArray() : null;
        }

        private static Item[] BuildItems(IEnumerable<PackageItem> bvPackageItems)
        {
            var items = new List<Item>();

            foreach (var bvPackageItem in bvPackageItems)
            {
                if (bvPackageItem.Quantity <= 0)
                    continue;

                var bvItem = LineItem.FindByBvin(bvPackageItem.LineItemBvin);
                items.Add(BuildItem(bvItem, bvPackageItem.Quantity));
            }

            return items.Count > 0 ? items.ToArray() : null;
        }

        private static string BuildMerchantUrl(string path, string query)
        {
            try
            {
                var uri = new UriBuilder(WebAppSettings.SiteSecureRoot);
                uri.Path += path;
                uri.Query = query;
                return uri.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static OrderResult BuildOrderResult(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var order = new Order();

            // Build the "simple" fields that have little or no chance of failing
            order.CustomerUrl = BuildCustomerUrl("Myaccount_Orders_Details.aspx", "id=" + bvOrder.Bvin);
            order.MerchantUrl = BuildMerchantUrl("BVAdmin/Orders/ViewOrder.aspx", "id=" + bvOrder.Bvin);
            order.OrderDate = bvOrder.TimeOfOrder;
            order.OrderId = bvOrder.Bvin;
            order.OrderNumber = bvOrder.OrderNumber;
            order.OrderStatus = bvOrder.StatusName;
            order.OrderTotal = bvOrder.GrandTotal;
            order.PaymentStatus = BuildPaymentStatus(bvOrder.PaymentStatus);
            order.ShippingStatus = BuildShippingStatus(bvOrder.ShippingStatus);

            try
            {
                order.BillingAddress = BuildAddress(bvOrder.BillingAddress, bvOrder.UserEmail);
                order.Charges = BuildCharges(bvOrder);
                order.Customer = BuildCustomer(bvOrder.User);
                order.Items = BuildItems(bvOrder);
                order.Payments = BuildPayments(bvOrder);
                order.Packages = BuildPackages(bvOrder.Bvin);

                var text = string.Format(ShippingAgentHelper.OrderExportedText, HttpContext.Current == null ? "(no IP in request)" : HttpContext.Current.Request.UserHostAddress);
                bvOrder.AddNote(new OrderNote { Note = text, NoteType = OrderNoteType.System });

                return new OrderResult { Success = true, Order = order };
            }
            catch (ApplicationException ex)
            {
                return new OrderResult { Success = false, Message = ex.Message, Order = order };
            }
            catch (Exception ex)
            {
                return new OrderResult { Success = false, Message = ex.ToString(), Order = order };
            }
        }

        private static PackageResult BuildPackageResult(BVSoftware.Bvc5.Core.Shipping.Package bvPackage)
        {
            var package = new Package();
            package.CustomerUrl = BuildCustomerUrl("Myaccount_Orders_Details.aspx", "id=" + bvPackage.OrderId);
            package.MerchantUrl = BuildMerchantUrl("BVAdmin/Orders/ShipOrder.aspx", "id=" + bvPackage.OrderId);
            package.OrderId = bvPackage.OrderId;
            package.PackageId = bvPackage.Bvin;

            try
            {
                // Add missing dimensions
                if (!bvPackage.HasShipped && bvPackage.Height == 0 && bvPackage.Weight == 0 && bvPackage.Length == 0)
                {
                    var bvShippingGroup = new ShippingGroup();
                    foreach (var bvPackageItem in bvPackage.Items)
                    {
                        bvShippingGroup.Items.Add(LineItem.FindByBvin(bvPackageItem.LineItemBvin));
                    }
                    bvShippingGroup.GenerateDimensions();
                    bvPackage.Height = bvShippingGroup.Height;
                    bvPackage.Length = bvShippingGroup.Length;
                    bvPackage.Width = bvShippingGroup.Width;
                    if (bvPackage.Weight == 0)
                        bvPackage.Weight = bvShippingGroup.Weight;
                }
                
                var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
                package.Destination = BuildShippingAddress(bvOrder.ShippingAddress, bvOrder.UserEmail);
                package.Height = GetDimensionInInches(bvPackage.Height, bvPackage.SizeUnits);
                package.Instructions = ShippingAgentHelper.StringOrNull(bvOrder.Instructions);
                package.Items = BuildItems(bvPackage.Items);
                package.Length = GetDimensionInInches(bvPackage.Length, bvPackage.SizeUnits);
                package.RateOptionName = ShippingAgentHelper.StringOrNull(bvOrder.ShippingMethodDisplayName);
                if (string.IsNullOrEmpty(package.RateOptionName))
                    package.RateOptionName = "(unknown)";
                if (!string.IsNullOrEmpty(bvOrder.ShippingProviderId) && !bvOrder.ShippingProviderId.Equals(NullProviderId, StringComparison.InvariantCultureIgnoreCase))
                {
                    var bvProvider = AvailableProviders.FindProviderById(bvOrder.ShippingProviderId);
                    package.RateProviderName = bvProvider.ProviderId.Equals(NullProviderId, StringComparison.InvariantCultureIgnoreCase) ? "(unknown)" : ShippingAgentHelper.StringOrNull(bvProvider.Name);
                }
                if (bvPackage.HasShipped)
                {
                    package.ShipDate = bvPackage.ShipDate;
                    package.ShipDateSpecified = true;
                }
                else
                {
                    package.ShipDateSpecified = false;
                }
                package.Shipped = bvPackage.HasShipped;
                package.TrackingNumbers = string.IsNullOrEmpty(bvPackage.TrackingNumber) ? null : bvPackage.TrackingNumber.Split(',');
                if (!string.IsNullOrEmpty(bvPackage.ShippingProviderId) && !bvPackage.ShippingProviderId.Equals(NullProviderId, StringComparison.InvariantCultureIgnoreCase))
                {
                    // If the provider has been deleted, this will return the
                    // the null provider, which does not support tracking.
                    var bvProvider = AvailableProviders.FindProviderById(bvPackage.ShippingProviderId);
                    if (bvProvider.SupportsTracking)
                    {
                        package.TrackingProviderId = bvPackage.ShippingProviderId;
                        package.TrackingProviderName = bvProvider.Name;
                    }
                }

                var bvWarehouse = WebAppSettings.SiteShippingAddress;
                var bvWarehouseEmail = WebAppSettings.ContactEmail;
                if (bvPackage.Items.Count > 0)
                {
                    var bvItem = LineItem.FindByBvin(bvPackage.Items[0].LineItemBvin);
                    if (bvItem != null && bvItem.Bvin.Equals(bvPackage.Items[0].LineItemBvin, StringComparison.InvariantCultureIgnoreCase))
                    {
                        if (bvItem.AssociatedProduct != null)
                        {
                            if (bvItem.AssociatedProduct.ShippingMode == ShippingMode.ShipFromManufacturer)
                            {
                                var bvManufacturer = Manufacturer.FindByBvin(bvItem.AssociatedProduct.ManufacturerId);
                                if (bvManufacturer != null &&
                                    bvManufacturer.Bvin.Equals(bvItem.AssociatedProduct.ManufacturerId, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    bvWarehouse = bvManufacturer.Address;
                                    bvWarehouseEmail = bvManufacturer.EmailAddress;
                                }
                            }
                            if (bvItem.AssociatedProduct.ShippingMode == ShippingMode.ShipFromVendor)
                            {
                                var bvVendor = Vendor.FindByBvin(bvItem.AssociatedProduct.VendorId);
                                if (bvVendor != null &&
                                    bvVendor.Bvin.Equals(bvItem.AssociatedProduct.VendorId, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    bvWarehouse = bvVendor.Address;
                                    bvWarehouseEmail = bvVendor.EmailAddress;
                                }
                            }
                        }
                    }
                }
                package.Warehouse = BuildAddress(bvWarehouse, bvWarehouseEmail);
                package.Weight = GetWeightInPounds(bvPackage.Weight, bvPackage.WeightUnits);
                package.Width = GetDimensionInInches(bvPackage.Width, bvPackage.SizeUnits);
                return new PackageResult { Success = true, Package = package };
            }
            catch (ApplicationException ex)
            {
                return new PackageResult { Success = false, Message = ex.Message, Package = package };
            }
            catch (Exception ex)
            {
                return new PackageResult { Success = false, Message = ex.ToString(), Package = package };
            }
        }

        private static PackageResult[] BuildPackages(string orderId)
        {
            var packages = new List<PackageResult>();
            foreach (var bvPackage in GetPackages(orderId))
                packages.Add(BuildPackageResult(bvPackage));
            return packages.Count > 0 ? packages.ToArray() : null;
        }

        private static NamedAmount[] BuildPayments(BVSoftware.Bvc5.Core.Orders.Order bvOrder)
        {
            var payments = new NamedAmountList();
            foreach (var bvPayment in bvOrder.Payments)
            {
                if (bvPayment.AmountCharged == 0) continue;
                payments[bvPayment.PaymentMethodName, bvPayment.FriendlyStatus].Amount += bvPayment.AmountCharged;
            }
            return payments.Count > 0 ? payments.ToArray() : null;
        }

        private static string BuildPaymentStatus(OrderPaymentStatus orderPaymentStatus)
        {
            switch (orderPaymentStatus)
            {
                case OrderPaymentStatus.Overpaid:
                    return "Overpaid";
                case OrderPaymentStatus.Paid:
                    return "Paid";
                case OrderPaymentStatus.PartiallyPaid:
                    return "Partially Paid";
                case OrderPaymentStatus.Unpaid:
                    return "Unpaid";
                default:
                    return string.Empty;
            }
        }

        private static string BuildShippingStatus(OrderShippingStatus orderShippingStatus)
        {
            switch (orderShippingStatus)
            {
                case OrderShippingStatus.FullyShipped:
                    return "Fully Shipped";
                case OrderShippingStatus.NonShipping:
                    return "Non Shipping";
                case OrderShippingStatus.PartiallyShipped:
                    return "Partially Shipped";
                case OrderShippingStatus.Unshipped:
                    return "Unshipped";
                default:
                    return string.Empty;
            }
        }

        private static ShippingAddress BuildShippingAddress(BVSoftware.Bvc5.Core.Contacts.Address bvAddress, string email)
        {
            if (bvAddress == null) return null;

            var address = new ShippingAddress();
            address.AddressId = string.IsNullOrEmpty(bvAddress.Bvin) ? GetUniqueAddressId(bvAddress) : bvAddress.Bvin;
            address.City = ShippingAgentHelper.StringOrNull(bvAddress.City);
            address.Company = ShippingAgentHelper.StringOrNull(bvAddress.Company);
            address.Country = ShippingAgentHelper.StringOrNull(bvAddress.CountryName);
            if (!string.IsNullOrEmpty(bvAddress.CountryBvin))
            {
                var country = Country.FindByBvin(bvAddress.CountryBvin);
                if (country != null && country.Bvin.Equals(bvAddress.CountryBvin, StringComparison.InvariantCultureIgnoreCase))
                    address.Country = country.IsoCode;
            }
            address.Email = email;
            address.Fax = ShippingAgentHelper.StringOrNull(bvAddress.Fax);
            address.FirstName = ShippingAgentHelper.StringOrNull(bvAddress.FirstName);
            address.LastName = ShippingAgentHelper.StringOrNull(bvAddress.LastName);
            address.Line1 = ShippingAgentHelper.StringOrNull(bvAddress.Line1);
            address.Line2 = ShippingAgentHelper.StringOrNull(bvAddress.Line2);
            address.Phone = ShippingAgentHelper.StringOrNull(bvAddress.Phone);
            address.PostalCode = ShippingAgentHelper.StringOrNull(bvAddress.PostalCode);
            address.Region = ShippingAgentHelper.StringOrNull(bvAddress.RegionName);
            address.Residential = bvAddress.Residential;

            return address;
        }

        private UpdateResult BuildUpdateResult(Update update, BVSoftware.Bvc5.Core.Shipping.Package bvPackage)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
            if (bvOrder == null || !bvOrder.Bvin.Equals(bvPackage.OrderId, StringComparison.InvariantCultureIgnoreCase))
                return new UpdateResult { Success = false, Message = "Invalid order ID." };

            var notes = new List<string>();

            if (update.Destination != null)
            {
                var bvCountry = GetCountryByName(update.Destination.Country);
                var bvRegion = GetRegionByName(bvCountry.Bvin, update.Destination.Region);

                if (!AddressesAreEqual(bvOrder.ShippingAddress, update.Destination, bvCountry.Bvin, bvRegion.Bvin))
                {
                    bvOrder.ShippingAddress.City = update.Destination.City;
                    bvOrder.ShippingAddress.Company = update.Destination.Company;
                    bvOrder.ShippingAddress.CountryBvin = bvCountry.Bvin;
                    bvOrder.ShippingAddress.CountryName = bvCountry.IsoCode;
                    bvOrder.ShippingAddress.Fax = update.Destination.Fax;
                    bvOrder.ShippingAddress.FirstName = update.Destination.FirstName;
                    bvOrder.ShippingAddress.LastName = update.Destination.LastName;
                    if (bvOrder.ShippingAddress.Line1 != update.Destination.Line1
                        || bvOrder.ShippingAddress.Line2 != update.Destination.Line2)
                    {
                        bvOrder.ShippingAddress.Line1 = update.Destination.Line1;
                        bvOrder.ShippingAddress.Line2 = update.Destination.Line2;
                        bvOrder.ShippingAddress.Line3 = string.Empty;
                    }
                    bvOrder.ShippingAddress.Phone = update.Destination.Phone;
                    bvOrder.ShippingAddress.PostalCode = update.Destination.PostalCode;
                    bvOrder.ShippingAddress.RegionBvin = bvRegion.Bvin;
                    bvOrder.ShippingAddress.RegionName = bvRegion.Name;
                    bvOrder.ShippingAddress.Residential = update.Destination.Residential;

                    if (BVSoftware.Bvc5.Core.Orders.Order.Update(bvOrder))
                        notes.Add("Updated order shipping address.");
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update order shipping address." };
                }
            }

            if (update.TrackingNumbers != null)
            {
                var trackingNumbers = string.Join(",", update.TrackingNumbers);
                if (!bvPackage.TrackingNumber.Equals(trackingNumbers, StringComparison.InvariantCultureIgnoreCase))
                {
                    bvPackage.TrackingNumber = trackingNumbers;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                        notes.Add(string.Format("Changed package tracking number to {0}.", trackingNumbers));
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package tracking number." };
                }
            }

            if (update.TrackingProviderId != null)
            {
                if (!bvPackage.ShippingProviderId.Equals(update.TrackingProviderId, StringComparison.InvariantCultureIgnoreCase))
                {
                    bvPackage.ShippingProviderId = update.TrackingProviderId;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                    {
                        var bvProvider = AvailableProviders.FindProviderById(update.TrackingProviderId);
                        notes.Add(string.Format("Changed package tracking provider to {0}.", bvProvider.Name));
                    }
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package tracking provider." };
                }
            }

            if (update.ShipDateSpecified)
            {
                if (bvPackage.ShipDate != update.ShipDate)
                {
                    bvPackage.ShipDate = update.ShipDate;
                    if (BVSoftware.Bvc5.Core.Shipping.Package.Update(bvPackage))
                        notes.Add(string.Format("Changed package ship date {0}.", update.ShipDate));
                    else
                        return new UpdateResult { Success = false, Message = "Cannot update package ship date." };
                }
                if (!bvPackage.HasShipped)
                {
                    if (bvPackage.Ship())
                        notes.Add("Changed package to shipped.");
                    else
                        return new UpdateResult { Success = false, Message = "Cannot ship package." };
                }
            }

            if (update.ShippingCostSpecified)
            {
                notes.Add(string.Format("Actual shipping cost is {0:c}.", update.ShippingCost));
            }

            if (notes.Count > 0)
            {
                var message = string.Join(" ", notes.ToArray());
                var text = string.Format(ShippingAgentHelper.OrderUpdateText, message);
                bvOrder.AddNote(new OrderNote { Note = text, NoteType = OrderNoteType.System });

                // Run the "Shipping Changed" workflow using a fresh copy of the order.
                var bvContext = new OrderTaskContext();
                bvContext.Order = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvPackage.OrderId);
                bvContext.UserId = bvOrder.UserID;
                Workflow.RunByBvin(bvContext, WebAppSettings.WorkflowIdShippingChanged);

                var result = new UpdateResult();
                result.Message = message;
                result.OrderResult = GetOrder(bvPackage.OrderId);
                result.Success = true;
                return result;
            }

            return new UpdateResult { Success = true, Message = "No change." };
        }

        private static Country GetCountryByName(string name)
        {
            var bvCountry = Country.FindByISOCode(name);
            if (bvCountry != null && bvCountry.IsoCode.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                return bvCountry;

            foreach (var country in Country.FindAll())
            {
                if (country.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                    return country;
            }

            return new Country { Bvin = string.Empty, DisplayName = name };
        }

        private static double GetDimensionInInches(decimal dimension, LengthType lengthType)
        {
            return Convert.ToDouble(lengthType == LengthType.Inches ? dimension : dimension * 0.393700787m);
        }

        private static IEnumerable<BVSoftware.Bvc5.Core.Shipping.Package> GetPackages(string bvin)
        {
            var bvOrder = BVSoftware.Bvc5.Core.Orders.Order.FindByBvin(bvin);

            PackageList packages = new PackageList();
            packages.AddRange(bvOrder.FindPackages());

            // Create suggested packages for items that are not already in other packages
            Collection<ShippingGroup> groups;
            try
            {
                groups = bvOrder.GetShippingGroups();
            }
            catch
            {
                throw new ApplicationException("Order " + bvOrder.OrderNumber +
                                               " contains at least 1 deleted product or product variation. Use BV Admin to remove the corresponding item from the order.");
            }

            foreach (var bvGroup in groups)
            {
                var bvPackage = new BVSoftware.Bvc5.Core.Shipping.Package();
                foreach (var bvItem in bvGroup.Items)
                {
                    var expectedQuantity = bvItem.Quantity - bvItem.QuantityShipped;
                    var totalQuantity = packages.GetTotalQuantity(bvItem.Bvin);
                    if (totalQuantity < expectedQuantity)
                        bvPackage.Items.Add(new PackageItem(bvItem.ProductId, bvItem.Bvin, expectedQuantity - totalQuantity));
                }

                if (bvPackage.Items.Count <= 0)
                    continue;

                bvGroup.GenerateDimensions();
                bvPackage.Bvin = Guid.NewGuid().ToString();
                bvPackage.CustomProperties.Add("Structured Solutions", "CreatedBy", "Shipping Agent");
                bvPackage.Description = "This package was created by Shipping Agent.";
                bvPackage.HasShipped = false;
                bvPackage.Height = bvGroup.Height;
                bvPackage.Length = bvGroup.Length;
                bvPackage.OrderId = bvOrder.Bvin;
                //BVC5SP3 bvPackage.ShippingMethodId is not stored in the database
                bvPackage.ShippingProviderId = bvOrder.ShippingProviderId;
                bvPackage.ShippingProviderServiceCode = bvOrder.ShippingProviderServiceCode;
                bvPackage.SizeUnits = WebAppSettings.SiteShippingLengthType;
                bvPackage.Weight = bvGroup.Weight;
                bvPackage.WeightUnits = WebAppSettings.SiteShippingWeightType;
                bvPackage.Width = bvGroup.Width;

                if (!bvOrder.AddPackage(bvPackage))
                    continue;

                packages.Add(bvPackage);
            }
            return packages;
        }

        private static Region GetRegionByName(string countryBvin, string name)
        {
            if (!string.IsNullOrEmpty(countryBvin))
            {
                foreach (var region in Region.FindByCountry(countryBvin))
                {
                    if (region.Abbreviation.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                        return region;
                    if (region.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                        return region;
                }
            }
            return new Region { Bvin = string.Empty, Name = name };
        }

        private static string GetUniqueAddressId(BVSoftware.Bvc5.Core.Contacts.Address bvAddress)
        {
            using (var ms = new MemoryStream())
            using (var writer = new StreamWriter(ms))
            {
                writer.Write(bvAddress.FirstName);
                writer.Write(bvAddress.LastName);
                writer.Write(bvAddress.Company);
                writer.Write(bvAddress.Line1);
                writer.Write(bvAddress.Line2);
                writer.Write(bvAddress.City);
                writer.Write(bvAddress.RegionName);
                writer.Write(bvAddress.CountryName);
                writer.Write(bvAddress.PostalCode);
                writer.Write(bvAddress.Phone);
                writer.Write(bvAddress.Fax);
                writer.Flush();
                ms.Seek(0, SeekOrigin.Begin);
                using (var crypto = new MD5CryptoServiceProvider())
                {
                    return Convert.ToBase64String(crypto.ComputeHash(ms));
                }
            }
        }

        private static double GetWeightInPounds(decimal weight, WeightType weightType)
        {
            return Convert.ToDouble(weightType == WeightType.Pounds ? weight : weight * 2.20462262M);
        }
    }
}
